Zustand Slices and Modular State Architecture
Zustand Slices & Modular State Architecture: Scaling a Collaborative Design Platform
1. Problem Statement
Case Study: DesignHub – Real-Time Collaborative Design
DesignHub is a Figma-like platform where:
-
Users can create, edit, and comment on design files in real time.
-
Each feature (files, users, comments, notifications, preferences) is managed by a different team.
-
State must be modular: easy to test, maintain, and scale as features grow.
-
Performance is critical—only components using changed state should re-render.
-
Teams want to use middleware (devtools, persistence, logging) on specific state slices.

The challenge:
How do you architect a global state system that is modular, type-safe, and scalable—so each feature team can own their slice, and the app remains fast and maintainable?
2. Learning Objectives
By the end of this tutorial, you will:
-
Understand what Zustand slices are and why they matter.
-
Architect modular state using slices for different features.
-
Combine slices into a single store with full type safety.
-
Apply middleware to specific slices or the whole store.
-
Test and maintain slices independently.
-
Avoid pitfalls like tight coupling and unnecessary re-renders.
3. Concept Introduction with Analogy
Analogy: The DesignHub Control Tower
-
Slices are like specialized teams in a control tower: one team manages flights, one manages weather, one manages communications.
-
Each team (slice) has its own dashboard, rules, and logs—but they all work together in the same tower (store).
-
If the weather team updates a forecast, only the weather dashboard changes—not the flight or comms dashboards.
4. Technical Deep Dive
A. What Are Zustand Slices?
-
A slice is a function that returns a piece of state and its actions, with its own types.
-
Slices are composed together to create the full store.
-
Each slice can have its own middleware, selectors, and tests.
Why slices?
-
Modularity: Each feature owns its state logic.
-
Scalability: Add or remove features without touching unrelated code.
-
Testability: Test slices in isolation.
-
Performance: Components subscribe only to the state they use.
B. Defining Slices: Example Types
// store/slices/userSlice.ts
export interface UserSlice {
user: { id: string; name: string } | null;
setUser: (user: { id: string; name: string }) => void;
clearUser: () => void;
}
export const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
});
// store/slices/fileSlice.ts
export interface File {
id: string;
name: string;
content: string;
}
export interface FileSlice {
files: File[];
addFile: (file: File) => void;
updateFile: (id: string, content: string) => void;
}
export const createFileSlice = (set, get) => ({
files: [],
addFile: (file) => set((state) => ({ files: [...state.files, file] })),
updateFile: (id, content) =>
set((state) => ({
files: state.files.map((f) =>
f.id === id ? { ...f, content } : f
),
})),
});
C. Combining Slices into a Single Store
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { createUserSlice, UserSlice } from './slices/userSlice';
import { createFileSlice, FileSlice } from './slices/fileSlice';
type DesignHubStore = UserSlice & FileSlice;
export const useDesignHubStore = create<DesignHubStore>()(
devtools(
persist(
(set, get) => ({
...createUserSlice(set, get),
...createFileSlice(set, get),
// Add more slices here
}),
{ name: 'designhub-store' }
)
)
);
- Order matters: Middleware like
devtools
andpersist
can wrap the whole store or individual slices.
D. Using Slices in Components
import { useDesignHubStore } from './store';
function UserProfile() {
const user = useDesignHubStore((s) => s.user);
const setUser = useDesignHubStore((s) => s.setUser);
if (!user) return <button onClick={() => setUser({ id: 'u1', name: 'Alex' })}>Login</button>;
return <div>Welcome, {user.name}</div>;
}
function FileList() {
const files = useDesignHubStore((s) => s.files);
const addFile = useDesignHubStore((s) => s.addFile);
return (
<div>
<button onClick={() => addFile({ id: Date.now().toString(), name: 'New', content: '' })}>
Add File
</button>
<ul>
{files.map((f) => (
<li key={f.id}>{f.name}</li>
))}
</ul>
</div>
);
}
E. Testing and Maintaining Slices
-
Slices can be tested independently by calling their factory functions with mock
set
andget
. -
Example (Jest)
import { createUserSlice } from './userSlice';
test('setUser sets user', () => {
let state = { user: null };
const set = (fn) => { state = { ...state, ...fn(state) }; };
const slice = createUserSlice(set);
slice.setUser({ id: 'u2', name: 'Sam' });
expect(state.user.name).toBe('Sam');
});
5. Step-by-Step Data Modeling & Code Walkthrough
A. Create Feature Slices
// store/slices/commentSlice.ts
export interface Comment {
id: string;
fileId: string;
author: string;
text: string;
}
export interface CommentSlice {
comments: Comment[];
addComment: (comment: Comment) => void;
getCommentsByFile: (fileId: string) => Comment[];
}
export const createCommentSlice = (set, get) => ({
comments: [],
addComment: (comment) => set((state) => ({ comments: [...state.comments, comment] })),
getCommentsByFile: (fileId) => get().comments.filter((c) => c.fileId === fileId),
});
B. Combine All Slices in the Store
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { createUserSlice, UserSlice } from './slices/userSlice';
import { createFileSlice, FileSlice } from './slices/fileSlice';
import { createCommentSlice, CommentSlice } from './slices/commentSlice';
type DesignHubStore = UserSlice & FileSlice & CommentSlice;
export const useDesignHubStore = create<DesignHubStore>()(
devtools(
persist(
(set, get) => ({
...createUserSlice(set, get),
...createFileSlice(set, get),
...createCommentSlice(set, get),
}),
{ name: 'designhub-store' }
)
)
);
C. Using Slices in the App
function CommentsPanel({ fileId }) {
const comments = useDesignHubStore((s) => s.getCommentsByFile(fileId));
const addComment = useDesignHubStore((s) => s.addComment);
return (
<div>
<ul>
{comments.map((c) => (
<li key={c.id}>{c.author}: {c.text}</li>
))}
</ul>
<button onClick={() => addComment({ id: Date.now().toString(), fileId, author: 'Alex', text: 'Hello!' })}>
Add Comment
</button>
</div>
);
}
6. Interactive Challenge / Mini-Project
Your Turn!
-
Create a
notificationsSlice
:-
Fields:
notifications: { id: string; message: string; read: boolean }[]
-
Actions:
addNotification
,markAsRead
,clearNotifications
-
-
Add the slice to the main store.
-
Build a
NotificationsPanel
component that displays unread notifications and lets users mark them as read.
7. Common Pitfalls & Best Practices
Common Pitfalls & Best Practices (Zustand Slices)
Pitfall | Best Practice |
---|---|
Mixing unrelated state in one slice | Keep slices focused on a single feature |
Tight coupling between slices | Use actions/selectors, not direct state access |
Not typing slices | Always define interfaces for each slice |
Middleware order mistakes | Apply devtools /persist after combining slices |
Not testing slices independently | Test each slice with mock set /get |
8. Optional: Programmer’s Workflow Checklist
-
Define an interface and factory for each slice.
-
Combine slices in the main store with middleware.
-
Use selectors to subscribe only to needed state.
-
Test slices in isolation with mock set/get.
-
Document slice boundaries and responsibilities.